#!/usr/bin/env python
#/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# *   Mupen64plus - configure                                               *
# *   Mupen64Plus homepage: http://code.google.com/p/mupen64plus/           *
# *   Copyright (C) 2009 DarkJeztr                                          *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU General Public License as published by  *
# *   the Free Software Foundation; either version 2 of the License, or     *
# *   (at your option) any later version.                                   *
# *                                                                         *
# *   This program is distributed in the hope that it will be useful,       *
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
# *   GNU General Public License for more details.                          *
# *                                                                         *
# *   You should have received a copy of the GNU General Public License     *
# *   along with this program; if not, write to the                         *
# *   Free Software Foundation, Inc.,                                       *
# *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.          *
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

import sys
import subprocess
import os
import commands
from optparse import OptionParser, OptionGroup, OptionValueError, SUPPRESS_HELP

defaultFile = 'configure.gen'

defaultOpts = { 
'NOBLIGHT':'0','NOJTTL':'0','NO_RESAMP':'0','NODUMMY':'0','PLUGINDBG':'0',
'NOGLN64':'0','NORICE':'0','NOGLIDE':'0', 'NOHLERSP':'0','NOMINPUT':'0',
'DBG_CORE':'0', 'DBG_COUNT':'0', 'DBG_COMPARE':'0', 'DBG_PROFILE':'0',
'DBG' : '0', 'DBGSYM' : '0', 'LIRC' : '1', 'Z64' : '1' , 'GUI' : 'GTK2',
'PREFIX' : '/usr/local', 'SHAREDIR' : '$(PREFIX)/share/mupen64plus',
'BINDIR' : '$(PREFIX)/bin', 'LIBDIR':'$(SHAREDIR)/plugins',
'MANDIR' : '$(PREFIX)/man/man1','PROFILE':'0',
'CONFIGURE.GEN' : 'Included' }

defaultHeader = """# configure.gen
# File Auto-Generated by ./configure
# DO NOT MODIFY

"""

optionTable = [
# Format: ['option string','make variable','value to assign',
#          'Group Identifier','help string']
#   ** 'value=None' : option takes argument
['--enable-debuggui', 'DBG','1','OPT','add core debugger'],
['--disable-debuggui','DBG','0','OPT','remove core debugger'],
['--enable-lirc',    'LIRC','1','OPT','add lirc support'],
['--disable-lirc',   'LIRC','0','OPT','remove lirc support'],
['--with-nogui',   'GUI','NONE','OPT','build without GUI'],
['--with-gtk2gui', 'GUI','GTK2','OPT','build with GTK2 GUI'],
['--with-qt4gui',   'GUI','QT4','OPT','build with QT4 GUI'],

['--with-dummy','NODUMMY','0','PLG','enable dummy plugins'],
['--without-dummy','NODUMMY','1','PLG','disable dummy plugins'],
['--with-ricevideo','NORICE','0','PLG',"enable Rice's video plugin"],
['--without-ricevideo','NORICE','1','PLG',"disable Rice's video plugin"],
['--with-gln64video','NOGLN64','0','PLG',"enable glN64 video plugin"],
['--without-gln64video','NOGLN64','1','PLG',"disable glN64 video plugin"],
['--with-glidevideo','NOGLIDE','0','PLG',"enable Glide64 video plugin"],
['--without-glidevideo','NOGLIDE','1','PLG',"disable Glide64 video plugin"],
['--with-z64',        'Z64','1','PLG',"enable Ziggy's lle rsp plugin"],
['--without-z64',     'Z64','0','PLG',"disable Ziggy's lle rsp plugin"],
['--with-azimerrsp','NOHLERSP','0','PLG',"enable Azimer's hle rsp plugin"],
['--without-azimerrsp','NOHLERSP','1','PLG',"disable Azimer's hle rsp plugin"],
['--with-blightinput', 'NOBLIGHT','0','PLG','enable Blight-Input plugin'],
['--without-blightinput', 'NOBLIGHT','1','PLG','disable Blight-Input plugin'],
['--with-mupeninput', 'NOMINPUT','0','PLG','enable Mupen64 Input plugin'],
['--without-mupeninput', 'NOMINPUT','1','PLG','disable Mupen64 Input plugin'],
['--with-jttlaudio','NOJTTL','0','PLG','enable JTTL audio plugin'],
['--without-jttlaudio','NOJTTL','1','PLG','disable JTTL audio plugin'],
['--enable-jttlresample','NO_RESAMP','0','PLG','enable JTTL SINC resampler'],
['--disable-jttlresample','NO_RESAMP','1','PLG','disable JTTL SINC resampler'],

['--enable-debug', 'DBGSYM','1','DBG','add debugging symbols'],
['--disable-debug','DBGSYM','0','DBG','remove debugging symbols'],
['--enable-gprof','PROFILE','1','DBG','enable gprof profiling'],
['--disable-gprof','PROFILE','0','DBG','disable gprof profiling'],
['--enable-dbgcore','DBG_CORE','1','DBG','print r4300 debug printfs'],
['--disable-dbgcore','DBG_CORE','0','DBG','disable r4300 debug printfs'],
['--enable-corecount','DBG_COUNT','1','DBG','print r4300 instruction count (64bit only)'],
['--disable-corecount','DBG_COUNT','0','DBG','disable r4300 instruction count (64bit only)'],
['--enable-corecomp','DBG_COMPARE','1','DBG','enable r4300 core synched debugging'],
['--disable-corecomp','DBG_COMPARE','0','DBG','disable r4300 core synched debugging'],
['--enable-profiling','DBG_PROFILE','1','DBG','dump r4300 dynarec profiling data'],
['--disable-profiling','DBG_PROFILE','0','DBG','disable r4300 dynarec profiling data'],
['--enable-plugindebug','PLUGINDBG','1','DBG','enable extra debugging output for all plugins '],
['--disable-plugindebug','PLUGINDBG','0','DBG','hide extra debugging output for all plugins '],


['--prefix-dir','PREFIX',None,'PTH','sets the prefix for install'],
['--data-dir','SHAREDIR',None,'PTH','path to install shared data'],
['--binary-dir','BINDIR',None,'PTH','path to install the binaries'],
['--plugin-dir','LIBDIR',None,'PTH','path to install the plugins'],
['--man-dir','MANDIR',None,'PTH','path to the manual page'],
]

optionHeading = { 'OPT':'Optional Features','PLG':'Plugin Options',
                    'DBG':'Debugging Options', 'PTH':'Path Options'}

def checkDeps(config, options):
    """Checks dependancies, and disables options requiring unmet deps"""
    
    #first assemble the dependency trees
    
    pkgTest = ShellTest(['which','pkg-config'],'pkg-config not installed')
    gtkTest = ShellTest(['pkg-config','gtk+-2.0'],'gtk+-2.0 not installed')
    gtkTest.addDep(pkgTest)

    sdlTest = ShellTest(['which','sdl-config'],'SDL development libraries not installed')
    
    glewTest = CompileTest('GL/glew.h','-lGLEW','Glew not installed')
    ftglTest = ShellTest(['pkg-config','ftgl'],'Ftgl not installed')
    ftglTest.addDep(pkgTest)
    
    sdl_flags = commands.getoutput("sdl-config --cflags --libs").split()
    sdlttfTest = CompileTest('SDL/SDL_ttf.h',sdl_flags+['-lSDL_ttf'],'SDL-TTF development libraries not installed')
    
    resampTest = ShellTest(['pkg-config','samplerate'],'libsamplerate not installed')
    resampTest.addDep(pkgTest)
    
    z64Test = ShellTest()
    z64Test.addDep(glewTest)
    z64Test.addDep(ftglTest)
    z64Test.addDep(sdlTest)

    coreTest = ShellTest()
    coreTest.addDep(sdlTest)
    
    blightTest = ShellTest()
    blightTest.addDep(sdlTest)
    blightTest.addDep(sdlttfTest)
    
    #The following plugins share the core's dependancy on SDL, but are not tested as
    #configure aborts testing if the core's deps are not bet
    # glN64, ricevideo, glide64, jttlaudio, mupen64input

    dbgTest = CompileTest('dis-asm.h',message='binutils not installed')
    
    lircTest = CompileTest('lirc/lirc_client.h','-llirc_client','LIRC not installed')    

    # perform checks
    # note that the ENTIRE dependency tree should be assembled before any of these tests are performed
    if not coreTest:
        print "Cannot build mupen64plus: ", coreTest.failedDep()
        return False
    
    if(config['NOJTTL']=='0'):
        if(config['NO_RESAMP']=='0') and not resampTest:
            config['NO_RESAMP']='1'
            if(options.verbose):
                sys.stderr.write('Disabling JTTL SYNC resampling support: ')
                sys.stderr.write(resampTest.failedDep())
                sys.stderr.write('\n')

    if(config['NOBLIGHT'] =='0') and not blightTest:
        config['NOBLIGHT'] = '1'
        if(options.verbose):
            sys.stderr.write('Disabling Blight_Input Plugin: ')
            sys.stderr.write(blightTest.failedDep())
            sys.stderr.write('\n')
    
    if(config['Z64']=='1') and not z64Test:
        config['Z64'] = '0'
        if(options.verbose):
            sys.stderr.write('Disabling Z64 Plugin: ')
            sys.stderr.write(z64Test.failedDep())
            sys.stderr.write('\n')
    
    if(config['LIRC']=='1') and not lircTest:
        config['LIRC'] = '0'
        if(options.verbose):
            sys.stderr.write('Disabling LIRC support: ')
            sys.stderr.write(lircTest.failedDep())
            sys.stderr.write('\n')
    
    if(config['DBG']=='1'):
        if not dbgTest:
            config['DBG'] = '0'
            if(options.verbose):
                sys.stderr.write('Disabling Graphical Debugger: ')
                sys.stderr.write(dbgTest.failedDep())
                sys.stderr.write('\n')
        elif config['GUI']=='NONE':
            config['DBG'] = '0'
            if(options.verbose):
                sys.stderr.write('Disabling Graphical Debugger: ')
                sys.stderr.write('Must have a gui enabled')
                sys.stderr.write('\n')
            
    
    #make sure we have at least one of each plugin to proceed
    #Check for video plugins
    if(config['NODUMMY']=='1' and config['NOGLIDE']=='1'and
        config['Z64']=='0' and config['NORICE']=='1' and
        config['NOGLN64']=='1'):
        print "Cannot build mupen64plus: ",
        print "No video plugins selected"
        return False
    
    #Check for audio plugins
    if(config['NODUMMY']=='1' and config['NOJTTL']=='1'):
        print "Cannot build mupen64plus: ",
        print "No audio plugins selected"
        return False
    
    #Check for input plugins
    if(config['NODUMMY']=='1' and config['NOBLIGHT']=='1' and
        config['NOMINPUT']=='1'):
        print "Cannot build mupen64plus: ",
        print "No input plugins selected"
        return False
    
    #Check for rsp plugins
    if(config['Z64']=='0' and config['NOHLERSP']=='1'):
        print "Cannot build mupen64plus: ",
        print "No RSP plugins selected"
        return False
    
    return True

def printSettings(config):
    #print "\nCurrent Settings to write to", defaultFile
    print "Options:"
    if config['DBG']=='1': print "\tGraphical Debugger"
    if config['LIRC']=='1': print "\tLIRC Support"
    if config['DBGSYM']=='1': print "\tDebug symbols"
    if config['GUI']=='NONE': print "\tNo GUI"
    elif config['GUI']=='GTK2': print "\tGTK2 GUI"
    elif config['GUI']=='QT4': print "\tQT4 GUI"
    else:
        print "            BAD GUI OPTION"
    if config['DBG_CORE']=='1': print "\tR4300 debugging output"
    if config['DBG_COUNT']=='1': print "\tR4300 instruction count output"
    if config['DBG_COMPARE']=='1': print "\tR4300 core-synched debugging"
    if config['DBG_PROFILE']=='1': print "\tR4300 dynarec profiling output"
    if config['PLUGINDBG']=='1': print "\tPlugin debugging output"
    
    print "Plugins:"
    if config['Z64']=='1': print "\tZ64 RSP & Video"
    if config['NOGLN64']=='0': print "\tglN64 Video"
    if config['NORICE']=='0': print "\tRice Video"
    if config['NOGLIDE']=='0': print "\tGlide64 Video"
    if config['NOJTTL']=='0':
        if config['NO_RESAMP']=='0': print "\tJTTL Audio with SINC resampler"
        else: print "\tJTTL Audio without SINC resampler"
    if config['NOHLERSP']=='0': print "\tAzimer HLE RSP"
    if config['NOBLIGHT']=='0': print "\tBlight Input"
    if config['NOMINPUT']=='0': print "\tMupen64 Input"
    if config['NODUMMY']=='0':
        print "\tDummy Video\n\tDummy Audio\n\tDummy Input"

    print "Directories:"
    print "  Install (PREFIX):  ", config['PREFIX']
    print "  Shared (SHAREDIR): ", config['SHAREDIR']
    print "  Binary (BINDIR):   ", config['BINDIR']
    print "  Plugins (LIBDIR):  ", config['LIBDIR']
    print "  Manual (MANDIR):   ", config['MANDIR']
    
def constructParser(config):
    parser = OptionParser()
    for heading in optionHeading:
        group = OptionGroup(parser, optionHeading[heading])
        for entry in optionTable: 
            if entry[3]==heading:
                if entry[2]==None: # we have an option with an arg
                    group.add_option(entry[0], help=entry[4], metavar="DIR",
                        nargs=1, type='string', action='callback',
                        callback=config.setOption )
                else:
                    if entry[2]==defaultOpts[entry[1]]:
                        hlpstr=SUPPRESS_HELP
                    else: hlpstr=entry[4]
                    group.add_option(entry[0], help=hlpstr,
                        action='callback', callback=config.setOption )
        parser.add_option_group(group)
    
    option = OptionGroup(parser, 'Configure Script Options')
    option.add_option('-q', '--quiet', help='suppress output', 
                                action='store_false', dest='verbose', default=True)
    option.add_option('-n', '--nosave', help='do not save changes',
                                action='store_false', dest='save', default=True)
    option.add_option('-f', '--force', help='ignore errors during read',
                                action='store_true', dest='force', default=False)
    option.add_option('-p', '--previous', help='load previous settings',
                                action='store_true', dest='previous', default=False)
    option.add_option('-d', '--no-deps', help='skip dependancy checks',
                                action='store_false', dest='checkdeps', default=True)
    
    parser.add_option_group(option)

    return parser


class ConfigFile(dict):
    """Handler for make-compatable config files"""
        
    def setOption(self, option, opt, value, parser):
        for entry in optionTable:
            if opt==entry[0]:
                if entry[2]==None:
                    self[entry[1]]=value
                else:
                    self[entry[1]]=entry[2]
                return
        #Handle option mismatches - shouldn't be possible
        raise OptionValueError( ''.join(['Invalid option: ', opt]) )

    def initialize(self, defaultDict=defaultOpts):
        self.__loaderror = False
        self.clear()
        self.update( defaultDict )

    def load(self, fileName=defaultFile):
        FILE = None
        self.__loaderror = False
        try:
            FILE = open(fileName,'r')
            lineNum = 0
            for curLine in FILE:
                curLine = curLine.split('#')[0] #strip comments
                lineNum=lineNum+1
                if curLine.isspace() or len(curLine) < 1:
                    continue #increment counter and skip line if it's blank
                if (curLine.count('=') == 1):
                    curLineChunks = curLine.split('=')
                    if (len(curLineChunks[0]) == 0 
                            or curLineChunks[0].isspace()):
                        sys.stderr.write("ERROR: Blank key in {1} line: {0}\n"
                            .replace("{0}", str(lineNum)).replace('{1}',defaultFile) )
                        self.__loaderror = True
                    else:
                        self[curLineChunks[0].strip()]=curLineChunks[1].strip()
                else:
                    sys.stderr.write('ERROR: Bad assignment in {1} line: {0}\n'
                        .replace('{0}',str(lineNum)).replace('{1}',defaultFile) )
                    self.__loaderror = True
        finally:
            if(FILE <> None):
                FILE.close()
    
    def save(self, fileName=defaultFile, header=defaultHeader):
        try:
            FILE = open(fileName,'w+')
            FILE.writelines(header)
            for key in self:
                FILE.write(''.join([key ,' = ', self[key], '\n']))
        finally:
            if(FILE <> None):
                FILE.close()

    def getError(self):
        return self.__loaderror

    def __init__(self, fileName=defaultFile, defaultDict=defaultOpts):
        self.__loaderror = False
        self.initialize(defaultDict)
        try:
            self.load(fileName)
        except IOError:
            self.initialize(defaultDict)

class ShellTest:
    """Class to execute some shell command and store the results"""
    def __init__(self,command='pwd',message=''):
        self.message = message
        self.deps = []
        self.checked = False
        self.value = False
        self.__command = command
        
    def result(self):
        """Process dep to see if it passes or not, and does so recursively"""
        if(self.checked == False):
            #print ''.join(["Checking if ",self.message])
            self.checked = True
            try:
                self.value = (subprocess.call(self.__command,
                        stdout=subprocess.PIPE,stderr=subprocess.PIPE)) == 0
            except OSError:
                self.value = False
        return self.value
    
    def failedDep(self):
        """Returns string describing the reason this dep failed"""
        for dep in self.deps:
            if not dep:
                return dep.failedDep()
        return self.message

    def findDep(self, find):
        """Returns true if the dependancy 'find' is a dependancy of this dep"""
        if(self == find):
            return True
        for dep in self.deps:
            if dep.findDep(find):
                return True
        return False
        
    def addDep(self, dep):
        self.deps.append(dep)
        self.checked = False
        assert(dep.findDep(self) == False) #avoid circular dependancy graphs

    def __nonzero__(self):
        for dep in self.deps:
            if not dep:
                return False
        return self.result()

class CompileTest(ShellTest):
    """Class to build a test app and compile it with gcc"""
    def __init__(self,header='stdio.h',library='-lm',message=''):
        self.message = message
        self.deps = []
        self.checked = False
        self.value = False
        self.__header = header
        self.__library = library
        
    def result(self):
        """Gets result by creating tmp.c file and running gcc"""
        if(self.checked == False):
            #print ''.join(["Checking if ",self.message])
            self.checked = True
            try:
                try:
                    srcfile = open('./tmp.c','w')
                    main_func = "int main(int argc, char*argv[])"
                    srcfile.write(''.join(['#include<',self.__header,'>\n',main_func,'{}\n']))
                finally:
                    srcfile.close()
                try:
                    # allow __library to be either a string or a list
                    if isinstance(self.__library, list):
                        call = ['gcc','tmp.c']+self.__library+['-o','tmp.out']
                    else:
                        call = ['gcc','tmp.c', self.__library, '-o','tmp.out']
                        
                    self.value=(subprocess.call(call,
                                        stdout=subprocess.PIPE,stderr=subprocess.PIPE) == 0)
                except OSError:
                    assert("Could not execute gcc"==0)
            finally:
                try:
                    os.remove('./tmp.c')
                except OSError: 
                    pass
                try:
                    os.remove('./tmp.out')
                except OSError: 
                    pass
        return self.value

def main():
    #get options and apply new settings to configChanges
    configChanges = ConfigFile('',[])
    parser = constructParser(configChanges)
    (options,args) = parser.parse_args() 

    if len(args) > 0: #should be no extra args after parsing
        parser.error('try "./configure --help"')

    if(options.previous):
        config = ConfigFile() #load settings from existing
    else:
        config = ConfigFile('') #load default settings
        
    config.update(configChanges) #apply settings from command line

    if(options.checkdeps):
        if not checkDeps(config, options): #check that current settings have deps met
            print "\nconfigure: error: You can skip dependancy checks with '-d'"
            return
    
    if(options.verbose):
        printSettings(config)
        
    #do not write settings if we were told not to, or if settings were loaded with errors
    #unless forced to save
    if(options.save and (options.force or not config.getError()) ):
        if(options.verbose):
            print "\nSaving settings to:", defaultFile
        config.save()
    elif(config.getError()):
        print "NOT SAVING: Due to errors reading", defaultFile
        print "Use '--force' to override"


if __name__ == '__main__':
    main()
